package com.anasoft.os.s4j;

import org.junit.runner.Runner;
import org.junit.runner.notification.RunListener;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.InitializationError;

import com.anasoft.os.s4j.annotation.ErrorReporting;
import com.anasoft.os.s4j.annotation.SeleniumSession;
import com.anasoft.os.s4j.util.AnnotationFinder;
import com.thoughtworks.selenium.Selenium;

/**
 * JUnit {@link Runner} for running tests within a {@link Selenium} session.
 * <p>
 * This runner is configured by placing a {@link SeleniumSession} annotation
 * over your test classes.
 * 
 * <pre>
 * &#064;RunWith(SeleniumTestRunner.class)
 * &#064;SeleniumSession(
 *      browserString = "*firefox",
 *      browserUrl = "http://localhost:8080"
 * )
 * public class MySeleniumTest {
 *      ...
 * }
 * </pre>
 * 
 * Alternatively, you can use {@link SeleniumSession} as the meta-annotation
 * placed over your custom Selenium test annotations. This is the preferred
 * approach since you don't have to repeat the {@link SeleniumSession}
 * configuration options over and over again for your test classes.
 * 
 * <pre>
 * &#064;Retention(RetentionPolicy.RUNTIME)
 * &#064;Target(ElementType.TYPE)
 * &#064;Inherited
 * &#064;SeleniumSession(
 *      browserString = "*firefox",
 *      browserUrl = "http://localhost:8080"
 * )
 * public &#064;interface MySeleniumSession {
 * }
 * </pre>
 * 
 * Just place your custom Selenium annotation over your test classes
 * to share common {@link SeleniumSession} configuration options.
 * 
 * <pre>
 * &#064;RunWith(SeleniumTestRunner.class)
 * &#064;MySeleniumSession
 * public class MySeleniumTest {
 *      ...
 * }
 * </pre>
 * 
 * Test classes can use the {@link #getSelenium() getSelenium} method
 * to retrieve the current {@link Selenium} session (the session in
 * which all of their test methods are executed). This session is
 * automatically started by the runner before running your tests and
 * stopped after all tests have finished.
 * 
 * <pre>
 * &#064;RunWith(SeleniumTestRunner.class)
 * &#064;MySeleniumSession
 * public abstract class AbstractSeleniumTest {
 *      protected final Selenium selenium = SeleniumTestRunner.getSelenium();
 * }
 * 
 * // annotations are inherited from the parent class
 * public class MySeleniumTest extends AbstractSeleniumTest {
 *      &#064;Test
 *      public void testSomething() {
 *          ...
 *      }
 * }
 * </pre>
 * 
 * There is exactly one {@link Selenium} session opened for each runner
 * instance. The session is held as a thread-local variable inside the
 * runner, so you can freely run your Selenium tests in a multi-threaded
 * way.
 * <p>
 * All Selenium based runners share some common operations by implementing
 * the {@link SeleniumRunnerLogic} interface. The actual implementation
 * of this interface comes from the {@link #getRunnerLogic() getRunnerLogic}
 * method and can be overriden in case you want to customize the default
 * runner behavior.
 * 
 * @see SeleniumSession
 * @see SeleniumSuiteRunner
 * @see SeleniumRunnerLogic
 * 
 * @author vojtech.szocs
 */
public class SeleniumTestRunner extends BlockJUnit4ClassRunner implements SeleniumRunnerLogic {

    private static final ThreadLocal<Selenium> sessionHolder = new ThreadLocal<Selenium>();
    
    private final SeleniumSession sessionConfig;
    private final SeleniumRunnerLogic runnerLogic;
    
    public SeleniumTestRunner(Class<?> clazz) throws InitializationError {
        super(clazz);
        
        sessionConfig = AnnotationFinder.find(clazz, SeleniumSession.class);
        
        if (sessionConfig == null)
            throw new InitializationError(SeleniumSession.class.getSimpleName()
                    + " annotation must be placed over your test class for this runner: "
                    + clazz.getSimpleName());
        
        runnerLogic = getRunnerLogic();
    }
    
    /**
     * Override this method in case you want to customize the default
     * runner behavior.
     * 
     * @return {@link SeleniumRunnerLogic} implementation to be used.
     */
    protected SeleniumRunnerLogic getRunnerLogic() {
        return DefaultSeleniumRunnerLogic.Factory.getInstance();
    }
    
    /**
     * @see org.junit.runners.ParentRunner#run(org.junit.runner.notification.RunNotifier)
     */
    @Override
    public void run(final RunNotifier notifier) {
        SeleniumSessionManager.doInSession(sessionConfig, sessionHolder, new Runnable() {
            
            public void run() {
                notifier.addListener(getErrorReportingListener(
                        sessionConfig.errorReporting(), getSelenium()));
                
                SeleniumTestRunner.super.run(notifier);
            }
            
        }, runnerLogic.getSessionListener());
    }
    
    /**
     * Returns the reference to the current {@link Selenium} session.
     * <p>
     * Test classes can safely call this method in non-static blocks
     * since the session is started before the actual test class gets
     * instantiated by the runner.
     * 
     * @return Selenium API for executing commands on the current
     * session.
     */
    public static Selenium getSelenium() {
        return sessionHolder.get();
    }
    
    /**
     * @see com.anasoft.os.s4j.SeleniumRunnerLogic#getErrorReportingListener(com.anasoft.os.s4j.annotation.ErrorReporting, com.thoughtworks.selenium.Selenium)
     */
    public final RunListener getErrorReportingListener(ErrorReporting errorConfig, Selenium session) {
        return runnerLogic.getErrorReportingListener(errorConfig, session);
    }
    
    /**
     * @see com.anasoft.os.s4j.SeleniumRunnerLogic#getSessionListener()
     */
    public final SeleniumSessionListener getSessionListener() {
        return runnerLogic.getSessionListener();
    }
    
}
